// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/bind.h"
#include "base/files/scoped_temp_dir.h"
#include "base/guid.h"
#include "base/strings/strcat.h"
#include "base/synchronization/waitable_event.h"
#include "base/task/bind_post_task.h"
#include "base/task/post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "chromecast/cast_core/grpc/grpc_server.h"
#include "chromecast/cast_core/grpc/status_matchers.h"
#include "chromecast/cast_core/grpc/test_service.grpc.pb.h"
#include "chromecast/cast_core/grpc/test_service_handlers.h"
#include "chromecast/cast_core/grpc/test_service_stubs.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace cast {
namespace utils {
namespace {

using ::cast::test::StatusIs;

static const auto kEventTimeout = base::Seconds(1);
static const auto kServerStopTimeout = base::Seconds(1);

class GrpcUnaryTest : public ::testing::Test {
 protected:
  GrpcUnaryTest()
      : grpc_client_task_runner_(
            base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})) {
    CHECK(temp_dir_.CreateUniqueTempDir());
    endpoint_ = "unix:" +
                temp_dir_.GetPath()
                    .AppendASCII("cast-uds-" + base::GenerateGUID().substr(24))
                    .value();
  }

  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  scoped_refptr<base::SequencedTaskRunner> grpc_client_task_runner_;
  base::ScopedTempDir temp_dir_;
  std::string endpoint_;
};

TEST_F(GrpcUnaryTest, SyncUnaryCallSucceeds) {
  GrpcServer server;
  server.SetHandler<SimpleServiceHandler::SimpleCall>(
      base::BindLambdaForTesting(
          [](TestRequest request,
             SimpleServiceHandler::SimpleCall::Reactor* reactor) {
            EXPECT_EQ(request.foo(), "test_foo");
            TestResponse response;
            response.set_bar("test_bar");
            reactor->Write(std::move(response));
          }));
  server.Start(endpoint_);

  SimpleServiceStub stub(endpoint_);
  auto call = stub.CreateCall<SimpleServiceStub::SimpleCall>();
  call.request().set_foo("test_foo");
  auto response = std::move(call).Invoke();
  CU_ASSERT_OK(response);
  EXPECT_EQ(response->bar(), "test_bar");

  server.StopForTesting(kServerStopTimeout);
}

TEST_F(GrpcUnaryTest, SyncUnaryCallReturnsErrorStatus) {
  GrpcServer server;
  server.SetHandler<SimpleServiceHandler::SimpleCall>(
      base::BindLambdaForTesting(
          [&](TestRequest request,
              SimpleServiceHandler::SimpleCall::Reactor* reactor) {
            EXPECT_EQ(request.foo(), "test_foo");
            reactor->Write(
                grpc::Status(grpc::StatusCode::NOT_FOUND, "Not found"));
          }));
  server.Start(endpoint_);

  SimpleServiceStub stub(endpoint_);
  auto call = stub.CreateCall<SimpleServiceStub::SimpleCall>();
  call.request().set_foo("test_foo");
  auto response = std::move(call).Invoke();
  ASSERT_THAT(response.status(),
              StatusIs(grpc::StatusCode::NOT_FOUND, "Not found"));

  server.StopForTesting(kServerStopTimeout);
}

TEST_F(GrpcUnaryTest, SyncUnaryCallCancelledIfServerIsStopped) {
  GrpcServer server;
  base::WaitableEvent server_stopped_event;
  server.SetHandler<SimpleServiceHandler::SimpleCall>(
      base::BindLambdaForTesting(
          [&](TestRequest request,
              SimpleServiceHandler::SimpleCall::Reactor* reactor) {
            // Stop the server to trigger call cancellation.
            server.Stop(base::Milliseconds(100),
                        base::BindLambdaForTesting(
                            [&]() { server_stopped_event.Signal(); }));
          }));
  server.Start(endpoint_);

  SimpleServiceStub stub(endpoint_);
  auto call = stub.CreateCall<SimpleServiceStub::SimpleCall>();
  call.request().set_foo("test_foo");
  auto response = std::move(call).Invoke();
  ASSERT_THAT(response, StatusIs(grpc::StatusCode::UNAVAILABLE));

  // Need to wait for server to fully stop.
  {
    base::ScopedAllowBaseSyncPrimitivesForTesting allow_base_sync_primitives;
    ASSERT_TRUE(server_stopped_event.TimedWait(kEventTimeout));
  }
}

TEST_F(GrpcUnaryTest, AsyncUnaryCallSucceeds) {
  GrpcServer server;
  server.SetHandler<SimpleServiceHandler::SimpleCall>(base::BindPostTask(
      grpc_client_task_runner_,
      base::BindLambdaForTesting(
          [](TestRequest request,
             SimpleServiceHandler::SimpleCall::Reactor* reactor) {
            EXPECT_EQ(request.foo(), "test_foo");
            TestResponse response;
            response.set_bar("test_bar");
            reactor->Write(std::move(response));
          })));
  server.Start(endpoint_);

  SimpleServiceStub stub(endpoint_);
  auto call = stub.CreateCall<SimpleServiceStub::SimpleCall>();
  call.request().set_foo("test_foo");
  base::WaitableEvent response_received_event;
  std::move(call).InvokeAsync(
      base::BindLambdaForTesting([&](GrpcStatusOr<TestResponse> response) {
        CU_CHECK_OK(response);
        EXPECT_EQ(response->bar(), "test_bar");
        response_received_event.Signal();
      }));
  {
    base::ScopedAllowBaseSyncPrimitivesForTesting allow_base_sync_primitives;
    ASSERT_TRUE(response_received_event.TimedWait(kEventTimeout));
  }

  server.StopForTesting(kServerStopTimeout);
}

TEST_F(GrpcUnaryTest, AsyncUnaryCallReturnsErrorStatus) {
  GrpcServer server;
  server.SetHandler<SimpleServiceHandler::SimpleCall>(base::BindPostTask(
      grpc_client_task_runner_,
      base::BindLambdaForTesting(
          [&](TestRequest request,
              SimpleServiceHandler::SimpleCall::Reactor* reactor) {
            EXPECT_EQ(request.foo(), "test_foo");
            reactor->Write(
                grpc::Status(grpc::StatusCode::NOT_FOUND, "Not Found"));
          })));
  server.Start(endpoint_);

  SimpleServiceStub stub(endpoint_);
  auto call = stub.CreateCall<SimpleServiceStub::SimpleCall>();
  call.request().set_foo("test_foo");
  base::WaitableEvent response_received_event;
  std::move(call).InvokeAsync(base::BindPostTask(
      grpc_client_task_runner_,
      base::BindLambdaForTesting([&](GrpcStatusOr<TestResponse> response) {
        ASSERT_THAT(response.status(),
                    StatusIs(grpc::StatusCode::NOT_FOUND, "Not Found"));
        response_received_event.Signal();
      })));
  {
    base::ScopedAllowBaseSyncPrimitivesForTesting allow_base_sync_primitives;
    ASSERT_TRUE(response_received_event.TimedWait(kEventTimeout));
  }

  server.StopForTesting(kServerStopTimeout);
}

TEST_F(GrpcUnaryTest, AsyncUnaryCallCancelledIfServerIsStopped) {
  GrpcServer server;
  base::WaitableEvent server_stopped_event;
  server.SetHandler<SimpleServiceHandler::SimpleCall>(base::BindPostTask(
      grpc_client_task_runner_,
      base::BindLambdaForTesting(
          [&](TestRequest request,
              SimpleServiceHandler::SimpleCall::Reactor* reactor) {
            // Stop the server to trigger call cancellation.
            server.Stop(base::Milliseconds(100),
                        base::BindLambdaForTesting(
                            [&]() { server_stopped_event.Signal(); }));
          })));
  server.Start(endpoint_);

  SimpleServiceStub stub(endpoint_);
  auto call = stub.CreateCall<SimpleServiceStub::SimpleCall>();
  call.request().set_foo("test_foo");
  base::WaitableEvent response_received_event;
  std::move(call).InvokeAsync(base::BindPostTask(
      grpc_client_task_runner_,
      base::BindLambdaForTesting([&](GrpcStatusOr<TestResponse> response) {
        ASSERT_THAT(response, StatusIs(grpc::StatusCode::UNAVAILABLE));
        response_received_event.Signal();
      })));
  {
    base::ScopedAllowBaseSyncPrimitivesForTesting allow_base_sync_primitives;
    ASSERT_TRUE(response_received_event.TimedWait(kEventTimeout));

    // Need to wait for server to fully stop.
    ASSERT_TRUE(server_stopped_event.TimedWait(kEventTimeout));
  }
}

}  // namespace
}  // namespace utils
}  // namespace cast
